package drunkensailor.framework.support;

import java.awt.geom.Point2D;

import drunkensailor.framework.CellStates;
import drunkensailor.framework.Directions;
import drunkensailor.framework.Simulation;
import drunkensailor.framework.support.exceptions.InvalidMoveException;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A programatic representation of the ASCII map file; tracks sailor, ship, and obstacles and controls moves accordingly. 
 * @author William Barr
 * @author Henry Mei
 */
public class Map {

	private Cell[][] cells;
	private Point2D.Double sailorLocation;
	private Point2D.Double shipLocation;
	private String mapName;
	private Logger logger = Logger.getLogger("SailorLog");
	
	/**
	 * Constructs a new map from the given file.  
	 * @param mapName The file name which contains the ASCII representation of this map.  
	 */
	public Map(String mapName, Level logLevel)
	{
		cells = new Cell[23][80];
		sailorLocation = new Point2D.Double();
		shipLocation = new Point2D.Double();
		parseMapFile(mapName);
		this.mapName = mapName;
	}
	/**
	 * Gets the name of the file which contains this map.  
	 * @return The map name string.  
	 */
	public String getMapName()
	{
		return mapName;
	}
	/**
	 * Returns the location of the sailor.  
	 * @return The location of the sailor on the map.  
	 */
	public Point2D getSailorLocation()
	{
		return sailorLocation;
	}
	/**
	 * Gets the cell at a given coordinate pair.  
	 * @param x The X coordinate of the cell to return
	 * @param y The Y coordinate of the cell to return
	 * @return
	 */
	public Cell getCell(int x, int y)
	{
		return cells[x][y];
	}
/**
 * Returns the angle in which the ship is located, relative to the sailor.  Computes this information based on the sailor's current position and angle of attack toward the ship.  
 * @return The Direction to the ship.  
 */
public Directions getDirectionToShip()
	{
		//TODO Needs to use trig (arctan) to find degrees to ship then return the closest one.

		/*
		 * (m,n) is a coordinate pair representing the positive/negative/zero parity of 
		 * the row (m) and the column (n) of the relative position. This will make sense 
		 * after you read the next comment block, I promise. 
		 */
		int mSign;
		int nSign;
		
		/*
		 * xLens and yLens are the differences between the row and column coordinates of the ship's
		 * location and the sailor's location
		 */
		int xLens = (int) ((int) shipLocation.getY() - sailorLocation.getY());
		int yLens = (int) ((int) shipLocation.getX() - sailorLocation.getX());

		/* using the differences, we assign positive/negative/zero parity values for
		 * the relative position
		 * 1  -- positive
		 * 0  -- zero
		 * -1 -- negative
		 */
		 
		if(xLens > 0) 			mSign = 1;
		else if(xLens == 0)		mSign = 0;
		else					mSign = -1;
		
		if(yLens > 0) 			nSign = 1;
		else if(xLens == 0)		nSign = 0;
		else					nSign = -1;
		
		/*
		 * Logic that deals with the ship being on the primary cardinal directions
		 * (North, South, East, West)
		 * if any of the differences zero, it implies that they are either on the 
		 * same row or column ==> on a primary cardinal direction axis. The actual
		 * cardinal direction on the axis is determined by the parity of the relative
		 * position on the other axis. 
		 */
		if(nSign == 0)
		{
			if(mSign > 0)		return Directions.EAST;
			else				return Directions.WEST;
		}
		else if(mSign == 0)
		{
			if(nSign > 0)		return Directions.NORTH;
			else				return Directions.SOUTH;
		}
		// What if it's not on a primary direction axis?
		else
		{
			// gets the angle of the ship relative from the sailor
			double refAngle = this.arcTan(xLens, yLens);
			
			/*
			 * Reference Chart:
			 * If nSign > 0 && mSign > 0 Quadrant I
			 * If nSign > 0 && mSign < 0 Quadrant II
			 * If nSign < 0 && mSign < 0 Quadrant III
			 * If nSign < 0 && mSign > 0 Quadrant IV
			 */
			
			//1 3 2 4
			if(nSign > 0)
			{
				if(mSign > 0)
					return interpAngle(refAngle);
				else
					return interpAngle(180+refAngle);
			}
			else
			{
				if(mSign < 0)
					return interpAngle(180-refAngle);
				else
					return interpAngle(360-refAngle);
			}
		}
	
//		if(sailorLocation.getY() == shipLocation.getY())
//			if(sailorLocation.getX() > shipLocation.getX())
//				return Directions.WEST;
//			else
//				return Directions.EAST;
//		//Same column
//		else if(sailorLocation.getX() == shipLocation.getX())
//			if(sailorLocation.getY() > shipLocation.getY())
//				return Directions.NORTH;
//			else
//				return Directions.SOUTH;

	}
	
	/**
	 * Calculates the positive arctan value
	 * @param x, y are the lengths of the respective legs forming the triangle
	 * @return computes the positive arctan value for given inputs
	 */
	private double arcTan(int x, int y)
	{
		return Math.atan(Math.abs(y)/Math.abs(x))*180/Math.PI + 0.0;
	}
	
	/**
	 * Interprets which enum to output given an angle
	 * @param n is the angle to be interpreted
	 * @return the enum direction closest to the angle
	 */
	private Directions interpAngle(double n)
	{
		
		// Creates an array of the enum directions
		// Note: 0 and 360 are repeated to make sure Quadrant I and IV angles
		// are treated equally when comparing which axis is closer (NE vs. E
		// and SE vs. E)
		int[] compDir   = {0, 45, 90, 135, 180, 225, 270, 315, 360};
		int[] compAngle = new int[9];
		
		int min = 360;
		int ind = 10;
		
		// Calculates the smallest difference and corresponding index value
		for(int i = 0; i < 9; i++)
		{
			compAngle[i] = (int) Math.abs(n - compDir[i]);
			if(compAngle[i] < min)
			{
				min = compAngle[i];
				ind = i;
			}	
		}
		
		// Returns the closest enum direction by using the minimum index value
		switch(ind)
		{
		case 1:  return Directions.NORTHEAST; 
		case 2:  return Directions.NORTH; 
		case 3:  return Directions.NORTHWEST; 
		case 4:  return Directions.WEST;
		case 5:  return Directions.SOUTHWEST;
		case 6:  return Directions.SOUTH;
		case 7:  return Directions.SOUTHEAST;
		default: return Directions.EAST; 
		}	
	}
	

/**
 * Gets the size of the map.
 * @return The size of the map as a Point2D.  
 */
	public Point2D getSize()
	{
		return new Point2D.Double(cells.length, cells[0].length);
	}

/**
 * Attempts to move the sailor in the specified direction, if possible.
 * @param dir The direction in which to move the sailor.
 * @param moveNumber The applicable turn number (for tracking purposes).
 * @throws InvalidMoveException 
 * @throws InvalidMoveException Thrown when the move is impossible to complete (either out of bounds or into occupied cell).
 */
	public void moveSailorTo(Directions dir, int moveNumber) throws InvalidMoveException
	{
		int sailorX = (int) sailorLocation.getX();
		int sailorY = (int) sailorLocation.getY();

		if(dir == Directions.CENTER)
			return;
		else if(dir == Directions.NORTHWEST)
			if(isValidMove(sailorX-1, sailorY-1))
				moveSailorTo(sailorX-1, sailorY-1, moveNumber);
			else
				throw new InvalidMoveException();
		else if(dir == Directions.NORTH)
			if(isValidMove(sailorX-1, sailorY))
				moveSailorTo(sailorX-1, sailorY, moveNumber);
			else
				throw new InvalidMoveException();
		else if(dir == Directions.NORTHEAST)
			if(isValidMove(sailorX-1, sailorY+1))
				moveSailorTo(sailorX-1, sailorY+1, moveNumber);
			else
				throw new InvalidMoveException();
		else if(dir == Directions.WEST)
			if(isValidMove(sailorX, sailorY-1))
				moveSailorTo(sailorX, sailorY-1, moveNumber);
			else
				throw new InvalidMoveException();
		else if(dir == Directions.EAST)
			if(isValidMove(sailorX, sailorY+1))
				moveSailorTo(sailorX, sailorY+1, moveNumber);
			else
				throw new InvalidMoveException();
		else if(dir == Directions.SOUTHWEST)
			if(isValidMove(sailorX+1, sailorY-1))
				moveSailorTo(sailorX+1, sailorY-1, moveNumber);
			else
				throw new InvalidMoveException();
		else if(dir == Directions.SOUTH)
			if(isValidMove(sailorX+1, sailorY))
				moveSailorTo(sailorX+1, sailorY, moveNumber);
			else
				throw new InvalidMoveException();
		else
			if(isValidMove(sailorX+1, sailorY+1))
				moveSailorTo(sailorX+1, sailorY+1, moveNumber);
			else
				throw new InvalidMoveException();
	}
	

	/**
	 * Checks to see whether the specified move to the given X and Y coordinates is possible.  Covers both off-array errors and invalid cell movement errors.  
	 * @param x The X coordinate to check
	 * @param y The Y coordinate to check
	 * @return Defines whether this move is possible.  
	 */
	private boolean isValidMove(int x, int y)
	{
		logger.info("Sailor wants to move to position " + x + "," + y);
		if(x >=0 && y>=0) 
				if(x < cells.length && y <cells[0].length) 
					if(cells[x][y].getState() == CellStates.EMPTY || cells[x][y].getState() == CellStates.GOAL)
						return true;
					else{
						logger.log(Simulation.sailorInfoLevel, "Specified directional move is not valid; attempted to move to " + x + "," + y + " with cellstate "+ cells[x][y]);
						//System.out.println("Fails on empty state/goal state");
						//System.out.println("x:  " + x +", y:  " + y + ";" + cells[x][y]);
						return false;
					}
				else
				{
					logger.log(Simulation.sailorInfoLevel,"Array indices are out of bounds for attempted move to coordinates " + x + "," + y);
					System.out.println("Array would be OOB");
					return false;
				}
		else
		{
			logger.log(Simulation.sailorInfoLevel,"Array indices are out of bounds for attempted move to coordinates " + x + "," + y);
			System.out.println("x or y outside domain");
		}
			return false;
	}
/**
 * Attempts to move the sailor to the given x and y position on the map's grid.  
 * @param x The X coordinate to which the sailor should be moved.  
 * @param y The Y coordinate to which the sailor should be moved.  
 * @param moveNumber The turn number of this move, used for tracking purposes.  
 * @throws InvalidMoveException Thrown if the move specified is impossible.  
 */
	private void moveSailorTo(int x, int y, int moveNumber) throws InvalidMoveException
	{
		if(x>=0 && x<cells.length && y>=0 && y<cells[0].length)
		{
			cells[x][y].occupyCell(moveNumber);
			cells[(int)sailorLocation.getX()][(int)sailorLocation.getY()].vacateCell();
			sailorLocation.setLocation(x, y);
			logger.info("Sailor occupied position " + x + "," + y);
		}
		else
		{
			throw new InvalidMoveException();
		}
	}

	public Point2D getShipLocation()
	{
		return shipLocation;
	}
/**
 *  Collates the information in a one cell radius around the sailor and returns the information (in CellStates) to the caller in a matrix.  
 * @return The direct surroundings of the sailor in a 3x3 matrix.  
 */
	public CellStates[][] getSurroundings()
	{
		CellStates[][] toRet = new CellStates[3][3];
		logger.info("Building surroundings matrix.  Sailor Location:  " + sailorLocation + "; Ship Location:  " + shipLocation);
		
		int rowCounter = 0;
		int colCounter = 0;
		
		
		//System.out.println("Sailor:  " + sailorLocation);
		//System.out.println("Ship:  " + shipLocation);
		
		logger.fine("Building array from " + (sailorLocation.getX()-1) + "," + (sailorLocation.getY()-1) + " to " + (sailorLocation.getX()+1) +"," + (sailorLocation.getY()+1));
		for(int i = (int)sailorLocation.getX()-1; i<=(int)sailorLocation.getX()+1; i++)
		{
			
			colCounter = 0;
			for(int x = (int)sailorLocation.getY()-1; x<=(int)sailorLocation.getY()+1; x++)
			{
				if(((i < 0 || x < 0) || (i >=cells.length || x >=cells[0].length)) && (rowCounter < toRet.length && colCounter < toRet[0].length))
				{
					logger.finer("Set results matrix " + rowCounter + "," + colCounter + " to null as entry is outside bounds.");
					toRet[rowCounter][colCounter] = null;
				}
				else
				{
					if(rowCounter < toRet.length && colCounter < toRet[0].length)
					{
						logger.finer("Set results matrix " + rowCounter + "," + colCounter + " to "+ cells[i][x]);
						toRet[rowCounter][colCounter] = cells[i][x].getState();
					}
				}
				colCounter++;
			}
			rowCounter++;
		}
		return toRet;
	}

	/**
	 * Determines whether the specified move will intercept the ship so the simulation will terminate.  
	 * @param move The move to check, given from the navigate method in Sailor.  
	 * @return Whether the specified move will intercept the ship or not.  
	 * @throws InvalidMoveException Thrown if the specified move will move off the map or into an occupied cell.  
	 */
	public boolean willInterceptShip(Directions move) throws InvalidMoveException
	{
		
		int sailorX = (int) sailorLocation.getX();
		int sailorY = (int) sailorLocation.getY();

		if(move == Directions.CENTER)
			return false;
		else if(move == Directions.NORTHEAST)
			if(isValidMove(sailorX-1, sailorY+1))
				if(sailorX-1 == shipLocation.getX() && shipLocation.getY() == sailorY+1)
					return true;
				else
					return false;
			else
				throw new InvalidMoveException();
		else if(move == Directions.NORTH)
			if(isValidMove(sailorX-1, sailorY))
				if(sailorX-1 == shipLocation.getX() && shipLocation.getY() == sailorY)
					return true;
				else
					return false;
			else
				throw new InvalidMoveException();
		else if(move == Directions.NORTHWEST)
			if(isValidMove(sailorX-1, sailorY-1))
				if(sailorX-1 == shipLocation.getX() && shipLocation.getY() == sailorY-1)
					return true;
				else
					return false;
			else
				throw new InvalidMoveException();
		else if(move == Directions.WEST)
			if(isValidMove(sailorX, sailorY-1))
				if(sailorX == shipLocation.getX() && shipLocation.getY() == sailorY-1)
					return true;
				else
					return false;
			else
				throw new InvalidMoveException();
		else if(move == Directions.EAST)
			if(isValidMove(sailorX, sailorY+1))
				if(sailorX == shipLocation.getX() && shipLocation.getY() == sailorY+1)
					return true;
				else
					return false;
			else
				throw new InvalidMoveException();
		else if(move == Directions.SOUTHWEST)
			if(isValidMove(sailorX+1, sailorY-1))
				if(sailorX+1 == shipLocation.getX() && shipLocation.getY() == sailorY-1)
					return true;
				else
					return false;
			else
				throw new InvalidMoveException();
		else if(move == Directions.SOUTH)
			if(isValidMove(sailorX+1, sailorY))
				if(sailorX+1 == shipLocation.getX() && shipLocation.getY() == sailorY)
					return true;
				else
					return false;
			else
				throw new InvalidMoveException();
		else
			if(isValidMove(sailorX+1, sailorY+1))
				if(sailorX+1 == shipLocation.getX() && shipLocation.getY() == sailorY+1)
					return true;
				else
					return false;
			else
				throw new InvalidMoveException();
	}
	
	public String toString()
	{
		String toRet = "";
		
		for(Cell[] row : cells)
		{
			for(Cell temp : row)
			{
				if(temp.getState() == CellStates.GOAL)
					toRet+="X";
				else if(temp.getState() == CellStates.EMPTY && temp.getAccesses().size() > 0)
					toRet+=".";
				else if(temp.getState() == CellStates.EMPTY && temp.getAccesses().size() == 0)
					toRet+=" ";
				else if(temp.getState() == CellStates.OCCUPIED)
					toRet+="O";
			}
			toRet+="\n";
		}
		
		return toRet;
	}
/**
 * Reads in the ASCII map file and creates a map accordingly.  
 * @param mapName The filename of the map to parse. 
 */
	private void parseMapFile(String mapName) {
		try
		{
			logger.info("Beginning map parse of " + mapName);
			FileInputStream fstream = new FileInputStream("maps/" + mapName);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader reader = new BufferedReader(new InputStreamReader(in));

			String line;
			int lineCounter = 0;
			while((line = reader.readLine())!=null && lineCounter<cells.length)
			{
				logger.finest("Line Count:  " + lineCounter);
				char[] lineData = line.toCharArray();
				for(int i = 0; i<cells[0].length; i++)
				{
					//System.out.println("Setting "+lineCounter+","+i);
					if(i<lineData.length)
					{
						if(lineData[i] == 'X')
						{
							//System.out.println("Goal!");
							logger.fine("Setting the goal at location " + lineCounter + "," + i);
							shipLocation.setLocation(lineCounter, i);
							cells[lineCounter][i] = new Cell(CellStates.GOAL);
						}
						else if(lineData[i] == '*')
						{
							//System.out.println("Adding sailor start");
							logger.fine("Putting the sailor at location " + lineCounter + "," + i);
							sailorLocation.setLocation(lineCounter, i);
							cells[lineCounter][i] = new Cell(CellStates.EMPTY);
							cells[lineCounter][i].occupyCell(0);
						}
						else if(lineData[i] == 'O')
						{
							//System.out.println("Occupied cell");
							logger.finest("Putting obstacle at location " + lineCounter + "," + i);
							cells[lineCounter][i] = new Cell(CellStates.OCCUPIED);
						}
						else
						{
							//System.out.println("Empty Cell");
							logger.finest("Empty space defined:  " + lineCounter + "," + i);
							cells[lineCounter][i] = new Cell(CellStates.EMPTY);
						}
					}
					else
					{
						//System.out.println("Empty Cell (default)");
						logger.finest("Empty space undefined and added:  " + lineCounter + "," + i);
						cells[lineCounter][i] = new Cell(CellStates.EMPTY);
					}
				}
				lineCounter++;
				//System.out.println("Done!");
			}
			in.close();
		}
		catch(IOException e)
		{
			logger.logp(Level.WARNING, "Map", "parseMapFile", "Unable to access map file", e);
			//System.out.println(e);
		}
	}

}